﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the API Concepts Framework
 * http://apiconceptsframework.codeplex.com
 *
 * Copyright (C) 2014 eva Kühn, Thomas Scheller, Vienna University of Technology
 * http://www.complang.tuwien.ac.at/eva
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using ApiConceptsFramework.Contract;

namespace ApiConceptsFramework
{
	public static class ReflectionExtensions
	{
		public static IEnumerable<MemberInfo> GetMembersForComplexityCalculation(this MeasurableTarget target, bool staticMembers)
		{
			//- returns all public members, excluding constructors, either static or non-static
			//- removes methods from object (GetType(), GetHashcode(), ...)
			//- removes methods with special names, which actually cannot be seen from the outside (e.g. property getter/setter methods)
			var flags = BindingFlags.Public | BindingFlags.FlattenHierarchy | (staticMembers ? BindingFlags.Static : BindingFlags.Instance);
			return target.Type.GetMembers(flags).Where(m => 
				!(m is ConstructorInfo) && 
				(!(m is MethodInfo) || !((MethodInfo)m).IsSpecialName) &&
				!(m.DeclaringType == typeof(object)))
				.Concat(target.GetExtensionMethods());
		}

		public static bool IsStatic(this MemberInfo member)
		{
			if (member is MethodInfo)
				return ((MethodInfo)member).IsStatic;
			if (member is FieldInfo)
				return ((FieldInfo)member).IsStatic;
			if (member is PropertyInfo)
				return (((PropertyInfo)member).GetGetMethod(true) ?? ((PropertyInfo)member).GetSetMethod(true)).IsStatic;
			throw new ArgumentException("Unkown MemberInfo Type " + member.GetType());
		}

		public static bool IsExtensionMethod(this MemberInfo member)
		{
			return member.IsDefined(typeof(ExtensionAttribute), false);
		}


		private class ExtensionMethod
		{
			public MemberInfo Method { get; set; }
			public Type ExtendedType { get; set; }
			public string NameSpace { get; set; }
		}

		private static readonly Assembly[] DefaultAssemblies = new[] //these assemblies are referenced by default in a .Net project
			{
				typeof(int).Assembly, //mscorlib
				typeof(Uri).Assembly, //System
				typeof(Enumerable).Assembly, //System.Core
				typeof(System.Data.DataRow).Assembly, //System.Data
				typeof(System.Data.DataRowComparer).Assembly, //System.Data.DataSetExtensions
				typeof(System.Xml.XmlNode).Assembly, //System.Xml
				typeof(System.Xml.Linq.XAttribute).Assembly //System.Xml.Linq

			};

		private static readonly Dictionary<string, List<ExtensionMethod>> ExtensionMethods = new Dictionary<string, List<ExtensionMethod>>();

		private static IEnumerable<ExtensionMethod> GetOrCreateListOfAllExtensionMethods(List<Assembly> assemblies)
		{
			if (assemblies == null)
				assemblies = new List<Assembly>();
			var key = string.Join(",", assemblies.Select(a => a.FullName));
			
			List<ExtensionMethod> methods;
			if (!ExtensionMethods.TryGetValue(key, out methods))
			{
				methods = CreateListOfAllExtensionMethods(assemblies.Concat(DefaultAssemblies));
				ExtensionMethods.Add(key, methods);
			}
			return methods;
		}

		private static List<ExtensionMethod> CreateListOfAllExtensionMethods(IEnumerable<Assembly> assemblies)
		{
			var methods = assemblies
				.SelectMany(a => a.GetTypes())
				.Where(t => t.IsPublic && t.GetConstructors().Length == 0)
				.SelectMany(t => t.GetMethods(BindingFlags.Public | BindingFlags.Static).Select(m => new{t,m}))
				.Where(_ => _.m.IsStatic && _.m.IsExtensionMethod())
				.Select(_ => new ExtensionMethod{Method = _.m, ExtendedType = _.m.GetParameters()[0].ParameterType, NameSpace = _.t.Namespace})
				.ToList();
			return methods;
		}

		public static IEnumerable<MemberInfo> GetExtensionMethods(this MeasurableTarget target)
		{
			var methods = GetOrCreateListOfAllExtensionMethods(target.ReferencedAssemblies);
			return methods
				.Where(m => ((target.CurrentUsings != null && target.CurrentUsings.Contains(m.NameSpace)) ||
				             (target.Type.Namespace != null && target.Type.Namespace.StartsWith(m.NameSpace))) &&
				            (m.ExtendedType.IsAssignableFrom(target.Type) ||
				             m.ExtendedType.IsGenericTypeDefAndInheritedOrImplementedBy(target.Type)))
				.Select(m => m.Method);
		}

		public static IEnumerable<Type> GetBaseTypes(this Type type)
		{
			//returns a list of all base types going down the hierarchy
			while ((type = (type.BaseType)) != null && type != typeof(object))
			{
				yield return type;
			}
		}

		public static bool IsGenericTypeDefAndInheritedOrImplementedBy(this Type genericTypeDef, Type type)
		{
			//TODO could check generic type constraints for a completely exact recognition
			if (!(genericTypeDef.IsGenericType && genericTypeDef.GetGenericArguments()[0].IsGenericParameter)) //this seems to be the only way with a correct result to recognize that the type of the first parameter is generic 
				return false;
			genericTypeDef = genericTypeDef.GetGenericTypeDefinition();
			if (type.IsValueType)
				return false;
			bool result = genericTypeDef.IsInterface ? 
				type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == genericTypeDef) : 
				type.GetBaseTypes().Any(t => t.IsGenericType && t.GetGenericTypeDefinition() == genericTypeDef);
			//Console.WriteLine(genericTypeDef.ToPrintableString() + " -> " + type.ToPrintableString() + " = " + result);
			return result;
		}
	}
}
